%%%-------------------------------------------------------------------
%%% @author Viktor Green <viktor@student.gu.se>
%%% @copyright (C) 2011, Viktor Green
%%% @doc
%%% This module handles encoding and decoding of bencoded data
%%% @reference See <a href="http://en.wikipedia.org/wiki/Bencode">
%%% Bencode at Wikipedia </a> for more information about the format
%%% @end
%%% Created : 21 Oct 2011 by Viktor Green <viktor@student.gu.se>
%%%-------------------------------------------------------------------

-module(bencode).

-export([decode/1, encode/1, encode_to_binary/1]).

-type b_int() :: {integer, integer()}.
-type b_string() :: {string, string()}.
-type dictionary_item() :: {b_string(), b_term()}.
-type b_dictionary() :: {dictionary, [dictionary_item()]}.
-type b_list() :: {list, [b_term()]}.
-type b_term() :: b_int() | b_string() | b_list() | b_dictionary().


%%--------------------------------------------------------------------
%% @doc
%% Function takes a binary containing bencoded data and returns a
%% parsed
%% @end
%%--------------------------------------------------------------------
-spec decode(Input :: binary() | list()) -> b_term().
decode(Input) when is_binary(Input)->
    {Value, _Rest} = decode_inner(Input),
    Value;
decode(Input) when is_list(Input) ->
    decode(list_to_binary(Input)).

%%--------------------------------------------------------------------
%% @doc
%% Encode a tagged value into a bencoded binarystring
%% @see encode/1.
%% @end
%%--------------------------------------------------------------------
-spec encode_to_binary(TaggedValue :: b_term()) -> binary().
encode_to_binary(TaggedValue) ->
    list_to_binary(encode(TaggedValue)).

%%--------------------------------------------------------------------
%% @doc
%% Encoding a tagged value into a bencoded string
%% @end
%%--------------------------------------------------------------------
-spec encode(b_term()) -> string().
encode({integer, Value}) ->
    [$i | integer_to_list(Value)] ++ "e";
encode({string, Value}) ->
    integer_to_list(length(Value)) ++ [$: | Value];
encode({list, Value}) ->
    [$l | encode_list(Value)] ++ "e";
encode({dictionary, Value}) ->
    [$d | encode_dictionary(Value)] ++ "e".

%%--------------------------------------------------------------------
%% @doc
%% Recursive function to encode a list to a bencoded string
%% @end
%%--------------------------------------------------------------------
-spec encode_list(b_list() | []) -> string().
encode_list([]) ->
    "";
encode_list([Head | Tail]) ->
    encode(Head) ++ encode_list(Tail).

%%--------------------------------------------------------------------
%% @doc
%% Recuresive function to encode a "dictionary" (list with key-value
%% pairs) into a bencoded string
%% @end
%%--------------------------------------------------------------------
-spec encode_dictionary(b_dictionary() | []) -> string().
encode_dictionary([]) ->
    "";
encode_dictionary([{Key, Value} | Tail]) ->
    encode(Key) ++ encode(Value) ++ encode_dictionary(Tail).

%%--------------------------------------------------------------------
%% @doc
%% Function matches different bencode "signatures", and sends data to
%% corresponding parser
%% 
%% Will crash if the input does not conform to bencoding standards
%% @end
%%--------------------------------------------------------------------
-spec decode_inner(binary()) -> {b_term(), binary()}.
decode_inner(<<$i, Rest/binary>>) ->
    get_int(Rest, []);
decode_inner(<<$l, Rest/binary>>) ->
    get_list(Rest, []);
decode_inner(<<$d, Rest/binary>>) ->
    get_dictionary(Rest, []);
decode_inner(Binary) ->
    long_string(Binary, []).

%%--------------------------------------------------------------------
%% @doc
%% Function to parse the length of a bencoded string, and then forward
%% the data to the string parser to get the actual string
%% @end
%%--------------------------------------------------------------------
-spec long_string(binary(), list()) -> {b_string(), binary()}.
long_string(<<$:, Rest/binary>>, SizeSoFar) ->
    get_string(Rest, list_to_integer(lists:reverse(SizeSoFar)), []);
long_string(<<First, Rest/binary>>, SizeSoFar) ->
    long_string(Rest, [First | SizeSoFar]).

%%--------------------------------------------------------------------
%% @doc
%% Parse a bencoded integer
%% @end
%%--------------------------------------------------------------------
-spec get_int(binary(), list()) -> {b_int(), binary()}.
get_int(<<$e, Rest/binary>>, SoFar) ->
    Integer = lists:reverse(SoFar),
    {{integer, list_to_integer(Integer)}, Rest};
get_int(<<First, Rest/binary>>, SoFar) ->
    get_int(Rest, [First | SoFar]).

%%--------------------------------------------------------------------
%% @doc
%% Parse a bencoded list (by recyrsivley call decode_inner)
%% @see decode_inner/1
%% @end
%%--------------------------------------------------------------------
-spec get_list(binary(), list()) -> {b_list(), binary()}.
get_list(<<$e, Rest/binary>>, SoFar) ->
    {{list, lists:reverse(SoFar)}, Rest};
get_list(Binary, SoFar) ->
    {Item, Rest} = decode_inner(Binary),
    get_list(Rest, [Item | SoFar]).

%%--------------------------------------------------------------------
%% @doc
%% Parse a bencoded dictionary (by recursivley call decode_inner)
%% @see decode_inner/1
%% @end
%%--------------------------------------------------------------------
-spec get_dictionary(binary(), list()) -> {b_dictionary(), binary()}.
get_dictionary(<<$e, Rest/binary>>, SoFar) ->
    {{dictionary, lists:reverse(SoFar)}, Rest};
get_dictionary(Binary, SoFar) ->
    {Key, Rest} = decode_inner(Binary),
    {Value, NewRest} = decode_inner(Rest),
    get_dictionary(NewRest, [{Key, Value} | SoFar]).

%%--------------------------------------------------------------------
%% @doc
%% Parse a bencoded string
%% @end
%%--------------------------------------------------------------------
-spec get_string(Bin :: binary(), Size :: integer(), Acc :: list()) ->
			{b_string(), binary()}.
get_string(Bin, 0, Acc) ->
    {{string, lists:reverse(Acc)}, Bin};
get_string(<<First, Rest/binary>>, Size, Acc) ->
    get_string(Rest, Size - 1, [First | Acc]).

